<template>
  <div class="virtual-scroll-container" ref="scrollRef">
    <div :style="contentStyle" class="virtual-content-container">
      <template v-for="{ data, key } in renderList" :key="key">
        <slot :data="data" :width="normalizedLayout.itemWidth" :height="normalizedLayout.itemHeight" />
      </template>
    </div>
  </div>
</template>

<script lang="ts">
import { useElementSize, templateRef, useScroll } from '@vueuse/core'
import type { CSSProperties, PropType, SlotsType } from 'vue'
import { isNil, isObject, uuid } from 'shared'
import {
  normalizeLayout,
  type VirtualListDataSource,
  type VirtualListFinalData,
  type VirtualListLayout,
} from './shared'

export default defineComponent({
  name: 'VirtualList',
  props: {
    layout: {
      type: Object as PropType<VirtualListLayout>,
      required: true,
    },
    dataSource: {
      type: [Object, Array] as PropType<VirtualListDataSource>,
      default: () => [],
    },
    key: {
      type: String,
      default: 'key',
    },
  },
  slots: Object as SlotsType<{ default: { data: unknown; width: number; height: number } }>,
  setup(props) {
    const scrollRef = templateRef<HTMLDivElement>('scrollRef')
    const { width: containerWidth, height: containerHeight } = useElementSize(scrollRef)
    const { y: scrollTop } = useScroll(scrollRef)

    const normalizedLayout = computed(() => normalizeLayout(props.layout))

    const cache = new WeakMap<object, VirtualListFinalData>()
    function normalizeData(data: unknown, uk?: string): VirtualListFinalData {
      if (data instanceof Object) {
        let finalData = cache.get(data)
        if (!finalData) {
          const { key } = props

          cache.set(data, (finalData = { data, key: uk ?? (data as any)[key] ?? uuid() }))
        }
        return finalData
      }
      return { data, key: uuid() }
    }

    const fullList = computed<VirtualListFinalData[]>(() => {
      const { dataSource } = props
      if (Array.isArray(dataSource)) {
        return dataSource.map((item) => normalizeData(item))
      }
      if (isObject(dataSource)) {
        return Object.entries(dataSource).map(([prop, item]) => normalizeData(item, prop))
      }
      return []
    })

    const columns = computed(() => {
      const { columnGap, itemWidth } = normalizedLayout.value
      return Math.floor((containerWidth.value + columnGap) / (itemWidth + columnGap))
    })

    const rows = computed(() => Math.ceil(fullList.value.length / columns.value))

    const viewportRows = computed(() => {
      const { rowGap, itemHeight } = normalizedLayout.value
      return Math.ceil((containerHeight.value + rowGap) / (itemHeight + rowGap))
    })

    const renderRange = computed(() => {
      const { rowGap, itemHeight, overscan } = normalizedLayout.value

      let rowStart = Math.floor((scrollTop.value + rowGap) / (itemHeight + rowGap))
      const rowEnd = rowStart + overscan + viewportRows.value
      if (rowStart > overscan) {
        rowStart -= overscan
      }

      return { rowStart, rowEnd: Math.min(rowEnd, rows.value) }
    })

    const finalHeight = computed(() => {
      const { rowGap, itemHeight } = normalizedLayout.value
      const rowsValue = rows.value
      return itemHeight * rowsValue + (isNil(rowGap) || rowsValue < 2 ? 0 : rowGap * (rowsValue - 1))
    })

    const contentStyle = computed<CSSProperties>(() => {
      const { columnGap, rowGap, itemWidth, itemHeight } = normalizedLayout.value
      const { rowStart } = renderRange.value

      return {
        height: `${finalHeight.value}px`,
        gridTemplateColumns: `repeat(auto-fill, ${itemWidth}px)`,
        columnGap: `${columnGap}px`,
        rowGap: `${rowGap}px`,
        paddingTop: `${rowStart * (itemHeight + rowGap)}px`,
      }
    })

    const renderList = computed(() => {
      const { rowStart, rowEnd } = renderRange.value

      const colValue = columns.value

      const startIndex = rowStart * colValue
      const endIndex = startIndex + (rowEnd - rowStart) * colValue

      return fullList.value.slice(startIndex, endIndex)
    })

    return { normalizedLayout, contentStyle, renderList }
  },
})
</script>

<style lang="scss" scoped>
.virtual-scroll-container {
  height: 100%;
  overflow-y: auto;
}

.virtual-content-container {
  display: grid;
  grid-auto-rows: max-content;
  justify-content: center;
  box-sizing: border-box;
}
</style>
